using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using CI.Interfaces;
using System.Threading;
using System.Runtime.Serialization;
using CI.Mesopotamia;
using CI.Misc;

namespace CI.Organism
{
    /// <summary>Individual Organism to be run in a environment</summary>
    [DataContract(IsReference = true)]
    [KnownType("GetKnownTypes")]
    public class Organism : IOrganism
    {
        #region Organism properties

        private readonly TimeSpan _locationcachetime = new TimeSpan(0, 0, 0, 0, 100);
        private const int LOCATION_DECIMAL_PLACES_COMPARISON = 3;
        private static SimEnv SimEnv { get { return SimEnv.Instance; } }

        /// <summary>Organism's DNA</summary>
        [DataMember]
        public IDNA DNA { get; set; }

        /// <summary>Organism's Neural system</summary>
        [DataMember]
        public INeuralSystem NeuralSystem { get; set; }

        /// <summary>This organisms parents</summary>
        [DataMember]
        public SynchronizedCollection<IOrganism> ParentOrganisms
        {
            get { return _parentOrganisms; }
            set { _parentOrganisms = value; }
        }
        private SynchronizedCollection<IOrganism> _parentOrganisms = new SynchronizedCollection<IOrganism>();

        /// <summary>Active state of the Organism. If their eaten etc don't process them</summary>
        public ActiveState ActiveState
        {
            get
            {
                return _activeState;
            }
            set
            {
                _activeState = value;
            }
        }
        private ActiveState _activeState = ActiveState.Process;

        public event Action<ILocation> LocationChangeEvent;
        public void LocationChange(ILocation location)
        {
            if (LocationChangeEvent != null)
                LocationChangeEvent(location);
        }

        /// <summary>Organism guid</summary>
        [DataMember]
        public Guid Guid { get; set; }

        /// <summary>Organism energy level, when this gets to 0 the Organism is Dead</summary>
        public Int64 Energy
        {
            get
            {
                return _energy;
            }
            set
            {
                if (_energy != value)
                {
                    // fire the change with the difference
                    if (EnergyChangeEvent != null)
                    {
                        EnergyChangeEvent(value - _energy);
                    }

                    _energy = value;

                    // fire the set event
                    if (EnergySetEvent != null)
                    {
                        EnergySetEvent(_energy);
                    }

                    CheckIfDead();
                }
            }
        }

        [DataMember]
        private Int64 _energy;

        /// <summary>Organism Init energy level the Organism had at creation, which would be subtracted from its parents</summary>
        [DataMember]
        public Int64 InitEnergy { get; set; }

        /// <summary>Body mass energy, this is the energy left in the Organism once it is dead</summary>
        [DataMember]
        public Int64 BMEnergy { get; set; }

        /// <summary>Collision Impact Energy Transfer, this is how much is taken off a coliding Organism if it is the largest of the two Organism</summary>
        [DataMember]
        public Int64 CIET { get; set; }

        /// <summary>Intial Collision Impact Energy Transfer, this is how much is taken off a coliding Organism if it is the largest of the two Organism</summary>
        [DataMember]
        public Int64 InitCIET { get; set; }

        /// <summary>Calculated fitness value assigned by the fitness function</summary>
        [DataMember]
        public Int64 FitnessValue { get; set; }


        /// <summary>
        /// Has the organism been born yet, they could be in seed type situation and not had the zygote active yet
        /// Usually determined by checking the zygote..
        /// If not born then we dont have to do neural or fitness function
        /// </summary>
        public bool Born
        {
            get
            {
                // if we have already figured out its alive then return yes
                if (_born)
                    return true;

                // see if we can find it, if not still not born
                if ((Cells == null) || (Cells.FindPhysicalZygote() == null))
                    return false;

                // look up the state, this is not 100% accurate as the org may have been deleted before being marked as born
                // so check physical hosting environment as well
                if (Zygote().Active)
                {
                    _born = true;
                }
                else
                {
                    // check physical hosting environment
                    if ((Organisms.hostEnvironment != null) && (Organisms.hostEnvironment.HostingEnvironmentPhysical != null) && (Organisms.hostEnvironment.HostingEnvironmentPhysical.Registered(this)))
                        _born = true;
                }

                // return the state
                return _born;
            }
        }
        private bool _born;

        /// <summary>All the organisms in the environment</summary>
        [DataMember]
        public IOrganismCollection Organisms { get; set; }

        /// <summary>The cells in the Organism</summary>
        [DataMember]
        public ICellCollection Cells
        {
            get { return _cells; }
            set { _cells = value; }
        }
        private ICellCollection _cells = new CellCollection();

        /// <summary>HeartBeat count for the Organism</summary>
        public UInt32 HeartbeatCount
        {
            get { return _heartbeatCount; }
            set
            {
                // shouldnt happen but just in case
                if (value == UInt32.MaxValue)
                    throw new OrganismException("Heartbeat Maximum Error");

                _heartbeatCount = value;
            }
        }
        private UInt32 _heartbeatCount;

        /// <summary>Organisms Location, used in collison detection</summary>
        public ILocation Location
        {
            get
            {
                // if the Organism has been inserted into the environment gets its location
                if (HostingEnvironment.HostingEnvironmentPhysical.Registered(this))
                {
                    // if cached return that
                    if (_cachedLocationDateTime.Add(_locationcachetime) > DateTime.Now)
                        return _location;

                    // get the location
                    ILocation tempLocation = HostingEnvironment.HostingEnvironmentPhysical.GetLocation(this);

                    // just check to make sure it has not been deleted before we got this far as the _templocation would be incorrect 
                    // and set to empty, so return the old value
                    if (tempLocation.CompareAxis(new Location(), LOCATION_DECIMAL_PLACES_COMPARISON))
                        return _location;

                    // save the last cached time of the location
                    _cachedLocationDateTime = DateTime.Now;

                    // check to see if we have changed if so mark us as dirty
                    if (!(_location.CompareAxis(tempLocation, LOCATION_DECIMAL_PLACES_COMPARISON)))
                    {
                        LocationDirty = true;
                    }

                    // create a new location from the temp location that we just read
                    _location = new Location(tempLocation.XAxis, tempLocation.YAxis, tempLocation.ZAxis, _location.XRadius, _location.YRadius, _location.ZRadius);

                    if (LocationDirty)
                        LocationChange(_location);
                }

                return _location;
            }
            set
            {
                // todo : look into setting the location of the entity/Organism in the environment, i think we can only do this on startup
                // unless we destroy it and create it again?
                // _location = this.HostingEnvironment.HostingEnvironmentPhysical.SetLocation(this, value);

                // check to see if we have changed if so mark us as dirty
                if (!(_location.CompareAxis(value, LOCATION_DECIMAL_PLACES_COMPARISON)))
                    LocationDirty = true;

                LocationChange(value);

                // return the value
                _location = value;
            }
        }
        [DataMember]
        private ILocation _location = new Location(0, 0, 0, 0, 0, 0); // create with a neutral location

        private DateTime _cachedLocationDateTime;

        /// <summary>
        /// This indicates if the location has changed and also to reset the flag
        /// From this we can decide to redraw maps and calculate collisions
        /// </summary>
        public bool LocationDirty { get; set; }

        /// <summary>Hosting Environment of the Organism, used to ask for location etc.</summary>
        [DataMember]
        public IHostingEnvironment HostingEnvironment { get; set; }

        /// <summary>Energy Set Event</summary>
        public event SynapseIODelegate EnergySetEvent;
        /// <summary>Energy Change Event</summary>
        public event SynapseIODelegate EnergyChangeEvent;
        /// <summary>Heartbeat firing</summary>
        public event Action HeartbeatChangeEvent;
        /// <summary>Registration of the organism in the enviroment change firing</summary>
        public event Action RegistrationChangeEvent;
        /// <summary>Happens when the organism is born</summary>
        public event Action BornChangeEvent;

        #endregion

        /// <summary>
        /// Check if the energy level set kills the organism
        /// </summary>
        private void CheckIfDead()
        {
            // if the value get to 0 then the Organism is dead
            if (_energy <= 0)
            {
                // check and make sure its alive as we can change this value once its dead but dont want to terminate the Organism again
                if (Monitor.TryEnter(Organisms.Alive, Globals.CriticalAcquireLockTimeout))
                {
                    try
                    {
                        // see if alive
                        if (Organisms.Alive.Contains(this))
                        {
                            Terminate("Organism Energy reserve is 0 and hence dead");
                        }
                    }
                    finally
                    {
                        Monitor.Exit(Organisms.Alive);
                    }
                }
                else
                {
                    throw new OrganismException("Unable to acquire lock on this.Organisms.Alive");
                }
            }
        }


        public void RegistrationChange()
        {
            if (RegistrationChangeEvent != null)
                RegistrationChangeEvent();
        }
        public void BornChange()
        {
            if (BornChangeEvent != null)
                BornChangeEvent();
        }

        /// <summary>Heart beat functioned triggered by environment to be used for dna processing and as a 
        /// time/instance counter.
        /// Also take care of the energy levels and fire any energy neurons attached to them
        /// </summary>
        public void Heartbeat()
        {
            // start timing
#if DEBUG
            var sw = new System.Diagnostics.Stopwatch();
            sw.Start();

            SimEnv.TraceLogger.LogDebug("Heartbeat - Start");
            SimEnv.TraceLogger.LogDebug(string.Format("Organism guid {1} Heart beat firing Count {0}", HeartbeatCount, Guid));
#endif
            RaiseHeartBeatChange();

            // Make sure the Organism has some dna
            if (DNA == null)
                throw new OrganismException("DNA is Empty(null)");

            // Grow the Organism per heart beat
            DNA.Grow();

            // update class internals
            lock (this)
            {
                // increment the heartbeat count
                HeartbeatCount++;

                // neural heartbeat to calculate energy up or down
                NeuralHeartbeat();
            }
#if DEBUG
            // how long did it take
            sw.Stop();
            SimEnv.TraceLogger.LogDebug(string.Format("Heartbeat - End-  Elapsed Milliseconds : {0}", sw.ElapsedMilliseconds));
#endif

        }

        private void RaiseHeartBeatChange()
        {
            if (HeartbeatChangeEvent != null)
                HeartbeatChangeEvent();
        }

        /// <summary>
        /// Update the organism on its heartbeat... eg orgs with neural decrease in energy where non ones increment
        /// </summary>
        private void NeuralHeartbeat()
        {
            // if it has a brain then it losses energy
            if (NeuralSystem != null)
            {
                // decrement the energy counter but make sure its above 0
                Energy--;
            }
            else
            {
                // else let it gain energy as it is a food source if it is born
                if (Born)
                {
                    Energy++;
                }
            }
        }

        /// <summary>Terminate Organism, so move it to the dead collection</summary>
        public void Terminate(string reason)
        {
            SimEnv.TraceLogger.LogDebug("Terminating Organism: " + Guid + " " + reason);

            // clear the ciet this will allow dead org to be cleaned up by other organisms
            CIET = -1;

            if (Organisms == null)
                throw new OrganismException("Organism does not belong to Organism collection");

            // lock the collections to change it
            if (Monitor.TryEnter(Organisms.Alive, Globals.CriticalAcquireLockTimeout))
            {
                try
                {
                    if (Monitor.TryEnter(Organisms.Dead, Globals.CriticalAcquireLockTimeout))
                    {
                        try
                        {
                            // if its still in the alive list
                            if (Organisms.Alive.Contains(this))
                            {
                                // remove it from alive
                                Organisms.AliveRemove(this);

                                // add it to dead
                                Organisms.DeadAdd(this);
                            }

                        }
                        finally
                        {
                            Monitor.Exit(Organisms.Dead);
                        }
                    }
                    else
                    {
                        throw new OrganismException("Unable to acquire lock on Organisms.Dead");
                    }
                }
                finally
                {
                    Monitor.Exit(Organisms.Alive);
                }
            }
            else
            {
                throw new OrganismException("Unable to acquire lock on Organisms.Alive");
            }

        }

        /// <summary>Terminate Organism and move it to the inactive collection, not the dead collection like a normal terminate will do</summary>
        public void TerminateAndDestroy(string reason)
        {

            SimEnv.TraceLogger.LogDebug("Terminating and Destroying Organism: " + Guid + " " + reason);

            lock (this)
            {
                // remove it from the sim environment if its born
                if (Born)
                {
                    HostingEnvironment.RegisterPhysicalCell(Zygote(), ActionType.RemoveCell);
                }

                // clear the ciet this will allow dead org to be cleaned up by other organisms
                CIET = -1;
                // set it to inactive
                ActiveState = ActiveState.Ignore;

                if (Organisms == null)
                    throw new OrganismException("Organism does not belong to Organism collection");

                // lock the collections to change it
                // if its in the alive List then the kill it
                if (Monitor.TryEnter(Organisms.Alive, Globals.CriticalAcquireLockTimeout))
                {
                    try
                    {
                        if (Organisms.Alive.Contains(this))
                        {

                            if (Monitor.TryEnter(Organisms.Inactive, Globals.CriticalAcquireLockTimeout))
                            {
                                try
                                {
                                    // if its still in the alive list
                                    if (Organisms.Alive.Contains(this))
                                    {
                                        // remove it from alive
                                        Organisms.AliveRemove(this);

                                        // add it to dead
                                        Organisms.InactiveAdd(this);
                                    }
                                }
                                finally
                                {
                                    Monitor.Exit(Organisms.Inactive);
                                }
                            }
                            else
                            {
                                throw new OrganismException("unable to acquire lock on Organisms.Inactive");
                            }
                        }
                    }
                    finally
                    {
                        Monitor.Exit(Organisms.Alive);
                    }
                }
                else
                {
                    throw new OrganismException("Unable to acquire lock on Organisms.Alive");
                }

                // lock the collections to change it
                // if its in the dead List then the kill it
                if (Monitor.TryEnter(Organisms.Dead, Globals.CriticalAcquireLockTimeout))
                {
                    try
                    {
                        if (Organisms.Dead.Contains(this))
                        {
                            if (Monitor.TryEnter(Organisms.Inactive, Globals.CriticalAcquireLockTimeout))
                            {
                                try
                                {
                                    // if its still in the alive list
                                    if (Organisms.Dead.Contains(this))
                                    {
                                        // remove it from alive
                                        Organisms.DeadRemove(this);

                                        // add it to dead
                                        Organisms.InactiveAdd(this);
                                    }
                                }
                                finally
                                {
                                    Monitor.Exit(Organisms.Inactive);
                                }
                            }
                            else
                            {
                                throw new OrganismException("Unable to acquire lock on Organisms.Inactive");
                            }
                        }
                    }
                    finally
                    {
                        Monitor.Exit(Organisms.Dead);
                    }
                }
                else
                {
                    throw new OrganismException("Unable to acquire lock on Organisms.Dead");
                }
            }
        }

        /// <summary>
        /// Create the Organism in the hosting environment with a zero'd guid
        /// </summary>
        /// <param name="hostingEnvironment">set the hosting enviorment used for call backs</param>
        public Organism(IHostingEnvironment hostingEnvironment) : this(hostingEnvironment, Guid.Empty, 0, 0) { }

        /// <summary>
        /// Create the Organism in the hosting environment with the guid
        /// </summary>
        /// <param name="hostingEnvironment">set the hosting enviorment used for call backs</param>
        /// <param name="guid">guid to uniquely identify the Organism</param>
        /// <param name="initEnergy">Initial energy the Organism had at startup</param>
        /// <param name="initCIET">Init CIET</param>
        public Organism(IHostingEnvironment hostingEnvironment, Guid guid, long initEnergy, long initCIET)
        {
            #region Argument Checks
            if (hostingEnvironment == null)
                throw new OrganismException("hostingEnvironment is null");
            if (hostingEnvironment.Organisms == null)
                throw new OrganismException("organims collection in hosting environment is null");
            #endregion

            // save the hosting environment
            HostingEnvironment = hostingEnvironment;

            // save the guid
            Guid = guid;

            // from the hosting environment save the Organism collection it belong to
            Organisms = hostingEnvironment.Organisms;

            // clear the location
            Location = new Location();

            // set the initial energy
            InitCIET = CIET = initCIET;

            // save the init energy level
            InitEnergy = Energy = initEnergy;
        }

        /// <summary>
        /// Override the to string fuction to return the type and energy level
        /// </summary>
        /// <returns>string</returns>
        public override string ToString()
        {
            // core cell
            string zygote;
            // find the type if it has one
            if ((Cells != null) && (Cells.FindPhysicalZygote() != null))
            {
                zygote = Zygote().ToString();
            }
            else
            {
                zygote = "Error : finding zygote";
            }

            //  is it regerested
            string registered;
            if ((HostingEnvironment != null) && (HostingEnvironment.HostingEnvironmentPhysical != null))
            {
                registered = HostingEnvironment.HostingEnvironmentPhysical.Registered(this).ToString().Substring(0, 1);
            }
            else
            {
                registered = "Error : Unable to check for registered organism";
            }

            return string.Format("{0} : E={1} : HB={2} : R={3} : B={4} ", zygote, Energy, HeartbeatCount, registered, _born.ToString().Substring(0, 1));
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            EnergySetEvent = null;
            EnergyChangeEvent = null;
            HeartbeatChangeEvent = null;
            RegistrationChangeEvent = null;
            BornChangeEvent = null;

            if (NeuralSystem != null)
                NeuralSystem.Dispose();
        }

        /// <summary>
        /// do a equals comparison of one organism type to another only comparing the value types, no refference comparisons
        /// </summary>
        /// <param name="organism">organism to compare against</param>
        /// <returns>true if equal value types</returns>
        public bool EqualsValueTypes(IOrganism organism)
        {
            // now start checking each of the properties
            if (organism.ActiveState != ActiveState)
                return false;
            if (organism.CIET != CIET)
                return false;
            if (organism.InitCIET != InitCIET)
                return false;
            if (organism.HeartbeatCount != HeartbeatCount)
                return false;
            if (organism.BMEnergy != BMEnergy)
                return false;
            if (organism.Born != Born)
                return false;
            if (organism.Energy != Energy)
                return false;
            if (organism.InitEnergy != InitEnergy)
                return false;
            if (organism.FitnessValue != FitnessValue)
                return false;
            // if not phyiscal hosting environment we will get a error so create a mock one to read if we dont have one just for the comparison
            if (organism.HostingEnvironment.HostingEnvironmentPhysical == null)
            {
                organism.HostingEnvironment.HostingEnvironmentPhysical = new MockHostingEnvironmentPhysical();
                if (!(organism.Location.Equals(Location)))
                {
                    organism.HostingEnvironment.HostingEnvironmentPhysical = null;
                    return false;
                }

                // now clear it
                organism.HostingEnvironment.HostingEnvironmentPhysical = null;
            }
            else
            {
                // if this doesnt have a physical hosting environment make one
                if (HostingEnvironment.HostingEnvironmentPhysical == null)
                {
                    HostingEnvironment.HostingEnvironmentPhysical = new MockHostingEnvironmentPhysical();

                    if (!(organism.Location.Equals(Location)))
                    {
                        HostingEnvironment.HostingEnvironmentPhysical = null;
                        return false;
                    }
                }
                else
                {
                    if (!(organism.Location.Equals(Location)))
                    {
                        return false;
                    }
                }
            }
            if (organism.ParentOrganisms.Count != ParentOrganisms.Count)
                return false;

            // check cells
            if ((organism.Cells != null) && (Cells != null))
            {
                // see if their are the same number of cells in both organisms
                if (organism.Cells.Count != Cells.Count)
                    return false;

                // see if the cells match
                if (!Cells.EqualsValueTypes(organism.Cells))
                    return false;
            }
            else
            {
                // if one has cells and the other not then return false
                if (!((organism.Cells == null) && (Cells == null)))
                    return false;

            }

            // check parent organisms
            if ((ParentOrganisms != null) && (organism.ParentOrganisms != null))
            {
                if (ParentOrganisms.Count != organism.ParentOrganisms.Count)
                    return false;

                // make sure they exist in both
                foreach (IOrganism parentOrganism in ParentOrganisms)
                {
                    // look through the other organism list to make sure it exists
                    IOrganism foundOrganismParents = null;
                    foreach (IOrganism parent in organism.ParentOrganisms)
                    {
                        if (parent.Guid == parentOrganism.Guid)
                        {
                            foundOrganismParents = parentOrganism;
                            break;
                        }

                        if (foundOrganismParents == null)
                            return false;
                    }
                }
            }
            else
            {
                if (!((ParentOrganisms == null) && (organism.ParentOrganisms == null)))
                    return false;
            }

            // if they both have brains
            if ((NeuralSystem != null) && (organism.NeuralSystem != null))
            {
                // check neural system types are the same
                if (!NeuralSystem.GetType().Equals(organism.NeuralSystem.GetType()))
                    return false;
            }
            else
            {
                if (!((NeuralSystem == null) && (organism.NeuralSystem == null)))
                    return false;
            }

            // check dna
            if (!DNA.EqualsValueTypes(organism.DNA))
                return false;

            // if got this far its all good
            return true;
        }

        /// <summary>
        /// get the known types
        /// </summary>
        /// <returns></returns>
        public static Type[] GetKnownTypes()
        {
            return MSRBHost.GetKnownTypes();
        }

        /// <summary>
        /// Get the organisms zygote
        /// </summary>
        /// <returns></returns>
        public ICell Zygote()
        {
            if (Cells == null)
                return null;

            return Cells.FindPhysicalZygote();
        }

        public ReadOnlyCollection<SynapseIO> GetIOManifest()
        {
            var zygote = Zygote() as IPhysical;
            return zygote == null ? null : zygote.IOManifest;
        }

        /// <summary>
        /// Get the synapseIO with the corresponding binding name
        /// </summary>
        /// <param name="bindingName"></param>
        /// <returns></returns>
        public SynapseIO GetSynapseIO(string bindingName)
        {
            var zygote = Zygote() as IPhysical;
            return zygote == null ? null : zygote.GetSynapseIO(bindingName);
        }

        /// <summary>
        /// Get the synapseIO with the corresponding Guid
        /// </summary>
        /// <param name="guid"></param>
        /// <returns></returns>
        public SynapseIO GetSynapseIO(Guid guid)
        {
            var zygote = Zygote() as IPhysical;
            return zygote == null ? null : zygote.GetSynapseIO(guid);
        }
    }
}
